react 源码下一步

March 24, 2021

前言

前文提到了一个简单的 react 例子,结构如下所示

ReactDOM.render(
  <h1>Hello World!</h1>,
  document.getElementById('container')
);

只是实在简单呀,缺少 state,缺少状态的变化,于是用另外一个例子来继续研究:

class Hello extends React.Component {
  state = {
    hasWorld: true,
  }

  changeWorld = (e) => {
    console.log('isChanging:', this.state.hasWorld)
    this.setState({hasWorld: !this.state.hasWorld});
  }

  render() {
    return (
      <div onClick={this.changeWorld}>
        {this.state.hasWorld ? 'Hello World!' : 'Hello'}
      </div>
    )
  }
}
ReactDOM.render(
  <Hello />,
  document.getElementById('container')
);

本文将围绕这个简单的 class 展开研究。

Hello 的初始化

这里先介绍 Hello 组件的初始化过程,包括前面提到的 render/reconcilation 阶段,以及 commit 阶段。

render/reconcilation 阶段

这个阶段前面部分和之前博客介绍的很类似,创建 root,创建 current,创建 workInProgress 的 root。只是到了 beginWork 就开始不一样了。这个阶段可以看看下面的图片。

可以看到在 Step 11 也就是 updateHostRoot 之前都是一样的,只是在该函数的时候,由于 element 的 type 为函数,不是前一篇博客中的 'h1',所以会在 reconcileChildFibers 函数阶段创建 tag 为 ClassComponent 的 fiber。可以从上图中看到,其 type 为 element 的 hello 函数。

第二轮 beginWordk 的时候,对象为上面生成的 child,执行的 case 对应的函数如下:

function updateClassComponent(current, workInProgress, renderExpirationTime) {
  if (current === null) {
    if (workInProgress.stateNode === null) {
      // 构建实例
      constructClassInstance(
        workInProgress,
        workInProgress.pendingProps,
        renderExpirationTime,
      );
      mountClassInstance(workInProgress, renderExpirationTime);

      shouldUpdate = true;
    }
  }
  return finishClassComponent(
    current,
    workInProgress,
    shouldUpdate,
    hasContext,
    renderExpirationTime,
  );
}
function constructClassInstance(workInProgress, props, renderExpirationTime){
  const ctor = workInProgress.type;
  const context = emptyObject;
  const instance = new ctor(props, context);
  const state = workInProgress.memoizedState = instance.state || null;
  instance.updater = classComponentUpdater;
  workInProgress.stateNode = instance;
  instance._reactInternalFiber = workInProgress;
  return instance;
}

在 constructClassInstance 时候会 new 一个 Hello class,将 child.stateNode 指向该实例。这里有三个特别操作

  1. 同时给实例的 instance.updater = classComponentUpdater。这个方法在 setState 里面会用到。
  2. 给 child.memoizedState 添加上 instance 的 state,也就是 { hasWorld: true }。为以后更新提供前值。
  3. 给 instance 添加对应的 fiber

这上面的三条都在 setState 里面起到非常关键的作用。updateClassComponent 里面还有 shouldComponentUpdate 的判断,但是这里是首次加载,必然是 true 了。

mountClassInstance 里面则是从 child 里面往 instance 添加 props,ref,context 等属性,同时判断 instance 有无 componentDidMount 函数,有的话 child.effectTag |= Update;。从而在后面阶段可以执行 componentDidMount。

后面的 finishClassComponent,如下

function finishClassComponent(current, workInProgress, shouldUpdate, hasContext, renderExpirationTime) {
  const ctor = workInProgress.type;
  const instance = workInProgress.stateNode;
  const nextChildren = instance.render();
  workInProgress.effectTag |= PerformedWork;
  reconcileChildren(current, workInProgress, nextChildren);
  return workInProgress.child;
}

reconcileChildren 的功能在前一篇博客里面已经提到过,就是如果存在 nextChildren,会生成一个新的 fiber。给到按当前 workInProgress.child。如上图所示。这里我们称其为 render fiber.

第三轮 beginWordk 的时候,对于 render fiber 执行的 case 为 HostComponent,和上篇博客介绍的类似。最后其会结束 beginWork 的工作,开始 completeUnitOfWork。

由于有三个父子关系的 fiber。所以这里也会有三轮 completeUnitOfWork 循环。首先是底层的 render fiber。对于该 fiber,和前一篇博客类似,进入 completeWork,通过 createInstance 创建 DOM。第二轮 completeUnitOfWork 里面 tag 为 ClassComponent,主要修改 root fiber 的 firstEffect/lastEffect。

第三轮同样也是,至此进入 commit 阶段。

commit 阶段

commit 阶段也简单,如同前一篇博客提到的。同样也有三大循环,同样的在 commitPlacement 里面插入元素到真实 DOM 里面,只是都了一次循环查找的过程。

可以发现对于 Component 组件的渲染和上一篇博客的介绍到的有大同小异,无非就是 Component 形成一个 tag 为 ClassComponent 的 fiber,而且本身的 render 结果又会是一个 fiber。EffectTag 以及 firstEffect 和 lastEffect 的关系还是一样的。

setState 之后的变化

本例子的对 setState 的触发,是通过定时器来实现的,而不是上文中的例子,这也是为了减少分析复杂度。

class Hello extends React.Component {
  state = {
    hasWorld: true,
  }
  
  componentDidMount() {
    setTimeout(() =>{
      this.setState({hasWorld: false})
    }, 5000)
  }

  changeWorld = (e) => {
    console.log(333, this.state.hasWorld)
    this.setState({hasWorld: !this.state.hasWorld});
  }

  render() {
    return (
      <div onClick={this.changeWorld}>
        {this.state.hasWorld ? 'Hello World!' : 'Hello'}
      </div>
    )
  }
}
ReactDOM.render(
  <Hello />,
  document.getElementById('container')
)

先看看 setState

Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
enqueueSetState: function(inst, payload, callback) {
  // 获取对应的 fiber
  const fiber = inst._reactInternalFiber;
  // 依旧还是 Sync 也就是 1;
  const expirationTime = computeExpirationForFiber(currentTime, fiber);
  const update = createUpdate(expirationTime);
  update.payload = payload;
  enqueueUpdate(fiber, update, expirationTime);
  scheduleWork(fiber, expirationTime);
}

可以看出来正是调用了实例的 updater 属性,也就是上文中提到的创建实例时候添加上的属性。而 enqueueSetState 里面获取到的 fiber,其实也就是生成 instance 的fibier,下图中的 oldChild。

在 enqueueSetState 中和我们前文提到的 scheduleRootUpdate 是很像的,都会创建新的队列,并且都会用到 enqueueUpdate 与 scheduleWork。于是后面看到的过程很多都是之前提到过的。只是由于这一次是更新所以有了很多不同。

比如在构建新的 workInProgress tree 的时候,并不会创建新的 newWorkInProgresRoot,而是修改之前的 oldWorkInProgresRoot.alternate 也就是最上面的 current 这个 fiber。当然同时也会将 newWorkInProgresRoot 的部分属性改为 oldWorkInProgresRoot,或者是清为 null,可以在上图看到,为了方便其见,新画了一个 workInProgress tree,而不是沿用之前的 current,当然这两个是一个 fiber 哦

这里简述一下后面的过程,在第一轮 performSyncWork 里面会通过 cloneChildFibers 方式给 newWorkInProgresRoot 创建 newChild fiber,tag 同样为 ClassComponent,child 也是一样的。

在第二轮 performSyncWork 的时候,会在 beginWork 进入 updateClassComponent,需要 updateClassInstance,

function updateClassComponent(current, workInProgress, renderExpirationTime) {
  const shouldUpdate = updateClassInstance(current, workInProgress, renderExpirationTime);
  return finishClassComponent(current, workInProgress, shouldUpdate, hasContext, renderExpirationTime);
}
function updateClassInstance(current, workInProgress, renderExpirationTime) {
  const ctor = workInProgress.type;
  const instance = workInProgress.stateNode;
  const oldProps = workInProgress.memoizedProps;
  const newProps = workInProgress.pendingProps;
  const oldState = workInProgress.memoizedState;
  let updateQueue = workInProgress.updateQueue;
  if (updateQueue !== null) {
    processUpdateQueue(
      workInProgress,
      updateQueue,
      newProps,
      instance,
      renderExpirationTime,
    );
    newState = workInProgress.memoizedState;
  }
  const shouldUpdate =
    checkHasForceUpdateAfterProcessing() ||
    checkShouldComponentUpdate(
      workInProgress,
      oldProps,
      newProps,
      oldState,
      newState,
      newContext,
    );
  if(shouldUpdate) {
    if (typeof instance.componentWillUpdate === 'function') {
      instance.componentWillUpdate(newProps, newState, newContext);
    }
    if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
      instance.UNSAFE_componentWillUpdate(newProps, newState, newContext);
    }
    if (typeof instance.componentDidUpdate === 'function') {
      workInProgress.effectTag |= Update;
    }
    if (typeof instance.getSnapshotBeforeUpdate === 'function') {
      workInProgress.effectTag |= Snapshot;
    }
  }
  workInProgress.memoizedProps = newProps;
  workInProgress.memoizedState = newState;
}

updateClassInstance 函数中可以明显的看到 fiber 的新老 state 和 props 的更新,当然也包括 instance 的 state/props 更新。以及 component 独有的生命钩子的执行,包括 ShouldComponentUpdate/ComponentWillUpdate/UNSAFE_componentWillUpdate,甚至通过修改 effectTag 为以后 commit 阶段执行其他生命钩子做好铺垫。 由于 newChild 的 upDateQueue 在 enqueueSetState 时候被添加更新了,有了更新 updateQueue 的过程,同时 newChild 的 memoizedState 也会被更新为最新的 state;

updateClassComponent 后面的 finishClassComponent,先是通过 instance.render() 生成 children 也就是虚拟 DOM,再通过 reconcileChildren 同样的创建新的 newRender 这个fiber。只是这一次不再是简单的生成新的 fiber。这里存在一个新老对比的过程。

function reconcileSingleElement(returnFiber, currentFirstChild, element, expirationTime) {
  const key = element.key;
  let child = currentFirstChild;
  while (child !== null) {
    if (child.key === key) {
      deleteRemainingChildren(returnFiber, child.sibling);
      const existing = useFiber(
        child,
        element.type === REACT_FRAGMENT_TYPE
        ? element.props.children
        : element.props,
        expirationTime,
      );
      existing.return = returnFiber;
      return existing;
    }
  }
}

因为 oldChild 存在 oldRender,所以就会在给 newChild 生成 newRender 这个 fiber 的时候有一个对比 key 的过程。以前 reconcileChildren 时候由于 oldChild 并不存在 alternate,所以不进入上面比较的过程。如果 key 相同则通过 useFiber 生成新的 fiber。

后一轮的 beginWork 和初始化的时候差不多,这里就不提了。

diff 属性

在结束 beginWork 之后,进入 completeWork 过程。这里同样的由于 newRender 存在 alternate,所以存在一个比较过程。这里也是 diff 的重点。

function completeWork(current, workInProgress, renderExpirationTime) {
  const newProps = workInProgress.pendingProps;
  const type = workInProgress.type;
  switch (workInProgress.tag) {
    case HostComponent: {
      if (current !== null && workInProgress.stateNode != null) {
        const oldProps = current.memoizedProps;
        const instance = workInProgress.stateNode;
        const updatePayload = diffProperties(
          instance,
          type,
          oldProps,
          newProps,
          rootContainerInstance,
        );
        workInProgress.updateQueue = updatePayload;
        workInProgress.effectTag |= Update;
      }
      return null;
  }
}

存在 alternate,就意味着这是个更新,而 这里的 diffProperties 就是对比新老 DOM 的不同。其做法很是粗暴,通过 for in 的形式对比出两个新老 Props 的不同,当然我们这里重点是 children 这个属性。最后会得出一个 updatePayload 数组,其奇数为 key,偶数为新的值。最后再将其赋予给到 workInProgress.updateQueue,更重要的是修改 effectTag,这样可以在 commit 阶段被识别到这是一个更新。同时也修改 newChild 的 firstEffect/lastEffect 为 child。第二轮 completeWork 里面,会把 newWorkInProgresRoot 的 firstEffect/lastEffect 指向 newRender 这个 fiber。子 fiber 的 effects 会通过链表的形式被添加到父 fiber 的 effects 上面。如果有原先的存在的则通过 fiber 的 nextEffct 来传递,实现链表。

commit 阶段

在 commitRoot 里面由于 finishedWork.effectTag 为 0,所以三大循环前的 nextEffect = finishedWork.firstEffect。也就是前面的提到的 newRender。这样下来在 commitPlacement 里面,由于 effectTag 为 4,即 Update case,会进入 commitWork 更新元素。

function commitWork(current, finishedWork) {
  switch (finishedWork.tag) {
    case HostComponent:
      const instance = finishedWork.stateNode;
      const newProps = finishedWork.memoizedProps;
      const oldProps = current !== null ? current.memoizedProps : newProps;
      const type = finishedWork.type;
      const updatePayload = finishedWork.updateQueue;
      updateFiberProps(instance, newProps);
      updateProperties(instance, updatePayload, type, oldProps, newProps);
  }
}
function updateProperties(domElement, updatePayload) {
  for (let i = 0; i < updatePayload.length; i += 2) {
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    if (propKey === CHILDREN) {
      setTextContent(domElement, propValue);
    }
  }
}

commitWork 里面的 updateFiberProps 是负责更新该 dom 指向的 props,改为 newProps;后面则是直接修改 nodeValue,实现 DOM 的更新。

setState 与 初始化的不同

从 setState 开始就不一样了。

  1. 先是给老的 oldChild 添加 updateQueue,里面维护着一个 update,包含了新状态。
  2. 由于创建的 newChild 存在 alternate,在 beginWork 时候会更新队列以及相关的生命钩子。
  3. newRender 的创建充分考虑到 key 的复用性,key 相同则根据老的 fiber 来创建新的 fiber 就可以了。
  4. completeWork 阶段对于 updatePayload 数组的收集,为后面的 commit 阶段做了很好的铺垫。
  5. commit 阶段 commitAllHostEffects 函数里面由于 effectTag 为 Update,会进入 commitWork 导致直接的更新操作。

上面只是介绍了通过 componentDidMount 定时器方式触发 setState 的,实际上常用的是通过点击的方式触发 interactiveUpdates 更新,也就是文中开头的方式,在 click 事件之后发生,click 的时候会触发监听的事件 dispatchInteractiveEvent,最后触发 setState 函数来更新队列,但是最后并没有在 setState 阶段执行 beginWork。而是在监听函数里面触发另外执行的。